package cn.net.yugu.doraemon.redis.lock;

import cn.net.yugu.doraemon.redis.RedisUtils;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * RLock lock = RedisLock.getLock("xxx");
 * try {
 *     if (lock.tryLock()) {
 *         //... run lock
 *     }
 * } finally {
 *     lock.unlock();
 * }
 * @description: redis 分布式锁.
 * @author: wanghongli
 * @create: 2022-07-18 00:43:29
 */
@Slf4j
@SuppressWarnings("unchecked")
public class RedisLock implements RLock {
    private RedisTemplate<String, String> CLIENT_INSTANCE;

    private final String lockKey;

    private final String lockValue;

    private volatile boolean locked = false;

    private int retry = 5;

    private long DEFAULT_TIMEOUT = TimeUnit.DAYS.toSeconds(1);

    private static final RedisScript<Boolean> SETNX_AND_EXPIRE_SCRIPT;

    private Charset UTF_8 = Charset.forName("utf-8");

    static {
        StringBuilder sb = new StringBuilder("if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then ");
        sb.append(" redis.call('expire', KEYS[1], tonumber(ARGV[2])) ");
        sb.append(" return 1 ");
        sb.append("else ");
        sb.append(" return 0 ");
        sb.append("end");
        SETNX_AND_EXPIRE_SCRIPT = new RedisScript<>(sb.toString(), Boolean.class);
    }

    private static final RedisScript<Boolean> DEL_IF_GET_EQUALS;

    static {
        StringBuilder sb = new StringBuilder("if (redis.call('get', KEYS[1]) == ARGV[1]) then ");
        sb.append(" redis.call('del', KEYS[1]) ");
        sb.append(" return 1 ");
        sb.append("else ");
        sb.append(" return 0 ");
        sb.append("end");
        DEL_IF_GET_EQUALS = new RedisScript<>(sb.toString(), Boolean.class);
    }

    public static RLock getLock(String lockKey) {
        return new RedisLock(lockKey);
    }

    public static RLock getLock(String lockKey, Long timeoutSeconds) {
        return new RedisLock(lockKey, timeoutSeconds);
    }

    private RedisLock(String lockKey, Long timeoutSeconds) {
        super();
        Objects.requireNonNull(lockKey, " lock key can not be null");
        Objects.requireNonNull(timeoutSeconds, " time out seconds can not be null");
        CLIENT_INSTANCE = RedisUtils.getTemplate();
        this.DEFAULT_TIMEOUT = timeoutSeconds;
        this.lockKey = ("lock_" + lockKey).intern();
        this.lockValue = UUID.randomUUID().toString() + "." + System.currentTimeMillis();
    }

    private RedisLock(String lockKey) {
        super();
        Objects.requireNonNull(lockKey, " lock key can not be null");
        CLIENT_INSTANCE = RedisUtils.getTemplate();
        this.lockKey = ("lock_" + lockKey).intern();
        this.lockValue = UUID.randomUUID().toString() + "." + System.currentTimeMillis();
    }

    @Override
    public boolean tryLock() {
        synchronized (lockKey) {
            try {
                DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
                redisScript.setResultType(Boolean.class);
                redisScript.setScriptText(SETNX_AND_EXPIRE_SCRIPT.getScriptAsString());
                List<String> keys = new ArrayList<>();
                keys.add(lockKey);
                Object returnValue = CLIENT_INSTANCE.execute(redisScript, keys, lockValue, DEFAULT_TIMEOUT + "");
                if (log.isDebugEnabled()) {
                    log.debug(" lockKey is [{}] , lock value [{}] , result is [{}] ", lockKey,
                            lockValue, JSON.toJSONString(returnValue));
                }
                locked = SETNX_AND_EXPIRE_SCRIPT.parseResult(returnValue);
                return locked;
            } catch (Throwable e) {
                log.error(e.getMessage());
            }
        }
        return false;
    }

    @Override
    public boolean tryLock(final long timeOut, final TimeUnit unit) throws InterruptedException {
        long millis = unit.toMillis(timeOut);
        long base = System.currentTimeMillis();
        long now = 0;
        long waitTime = (millis - millis % retry) / retry;
        long lockSeconds = unit.toSeconds(timeOut);
        if (millis <= 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        while (true) {
            synchronized (lockKey) {
                long delay = millis - now;
                if (delay <= 0) {
                    return false;
                }
                try {
                    DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
                    redisScript.setResultType(Boolean.class);
                    redisScript.setScriptText(SETNX_AND_EXPIRE_SCRIPT.getScriptAsString());
                    List<String> keys = new ArrayList<>();
                    keys.add(lockKey);
                    Object returnValue = CLIENT_INSTANCE.execute(redisScript, keys, lockValue, lockSeconds + "");
                    if (log.isDebugEnabled()) {
                        log.debug(" lockKey is [{}] , lock value [{}] , result is [{}] ",
                                lockKey, lockValue, JSON.toJSONString(returnValue));
                    }
                    locked = SETNX_AND_EXPIRE_SCRIPT.parseResult(returnValue);
                    if (locked) {
                        if (log.isDebugEnabled()) {
                            log.debug(" successful to get redis lock from redis !!");
                        }
                        return true;
                    }
                } catch (Throwable e) {
                    log.error(e.getMessage());
                }
                if (log.isDebugEnabled()) {
                    log.debug("## lock failed !! ,lockKey={}, delay={}, waitTime={}", this.lockKey, delay, waitTime);
                }
                if (delay < waitTime) {
                    lockKey.wait(delay);
                } else {
                    lockKey.wait(waitTime);
                }
                now = System.currentTimeMillis() - base;
            }
        }
    }

    @Override
    public void unlock() {
        if (!locked) {
            return;
        }
        locked = false;

        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Boolean.class);
        redisScript.setScriptText(DEL_IF_GET_EQUALS.getScriptAsString());
        List<String> keys = new ArrayList<>();
        keys.add(lockKey);
        Object returnValue = CLIENT_INSTANCE.execute(redisScript, keys, lockValue);
        if (log.isDebugEnabled()) {
            log.debug("unlock lockKey={}, n={}", this.lockKey, JSON.toJSONString(returnValue));
        }
    }

    @Override
    public boolean holdByCurrentThread() {
        return locked;
    }

    private static class RedisScript<T> {
        private final String script;

        private final Class<T> resultType;

        private RedisScript(String script, Class<T> resultType) {
            this.script = script;
            this.resultType = resultType;
        }

        private Class<T> getResultType() {
            return resultType;
        }

        private T parseResult(Object resultValue) {
            Objects.requireNonNull(resultValue, "result value can not be null");
            return (T) JSON.parseObject(JSON.toJSONString(resultValue), SETNX_AND_EXPIRE_SCRIPT.getResultType());
        }

        private String getScriptAsString() {
            return script;
        }
    }
}
